﻿// "Therefore those skilled at the unorthodox
// are infinite as heaven and earth,
// inexhaustible as the great rivers.
// When they come to an end,
// they bagin again,
// like the days and months;
// they die and are reborn,
// like the four seasons."
// 
// - Sun Tsu,
// "The Art of War"

using System;
using System.IO;
using log4net.Core;
using log4net.Layout;

namespace Infrastructure.Logging.Layouts
{
    /// <summary>
    /// A Layout that renders only the Exception data from the logging event.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The data comes in three flavors:<br/>
    /// <b>Type:</b> the full type name of the exception (i.e. System.Exception)<br/>
    /// <b>Message:</b> the exception message<br/>
    /// <b>Full:</b> the full exception data as retrieved from exception.ToString().<br/>
    /// </para>
    /// <para>
    /// This Layout should only be used with appenders that utilize multiple
    /// layouts (e.g. <see cref="log4net.Appender.AdoNetAppender"/>).
    /// </para>
    /// </remarks>
    public class ExtendedExceptionLayout : LayoutSkeleton
    {
        /// <summary>
        /// Default constructor
        /// </summary>
        /// <remarks>
        /// <para>
        /// Constructs a ExceptionLayout
        /// </para>
        /// </remarks>
        public ExtendedExceptionLayout()
        {
            IgnoresException = false;
        }

        #region Implementation of IOptionHandler

        /// <summary>
        /// Activate component options
        /// </summary>
        /// <remarks>
        /// <para>
        /// Part of the <see cref="IOptionHandler"/> component activation
        /// framework.
        /// </para>
        /// <para>
        /// This method does nothing as options become effective immediately.
        /// </para>
        /// </remarks>
        override public void ActivateOptions()
        {
            // nothing to do.
        }

        #endregion

        #region Override implementation of LayoutSkeleton

        /// <summary>
        /// Gets the exception text from the logging event
        /// </summary>
        /// <param name="writer">The TextWriter to write the formatted event to</param>
        /// <param name="loggingEvent">the event being logged</param>
        /// <remarks>
        /// <para>
        /// Write the exception string to the <see cref="TextWriter"/>.
        /// The exception string is retrieved from <see cref="LoggingEvent.GetExceptionString()"/>.
        /// </para>
        /// </remarks>
        override public void Format(TextWriter writer, LoggingEvent loggingEvent)
        {
            if (loggingEvent == null) throw new ArgumentNullException( "loggingEvent" );

            // depends on the output type write the data
            switch (mOutputType)
            {
                case ExceptionOutput.Type :
                    writer.Write( ExtractExceptionType( loggingEvent ) );
                    break;

                case ExceptionOutput.Message :
                    writer.Write( ExtractExceptionMessage( loggingEvent ) );
                    break;

                case ExceptionOutput.Full :
                    writer.Write( loggingEvent.GetExceptionString() );
                    break;
            }
        }

        /// <summary>
        /// Extract exception type.
        /// </summary>
        /// <remarks>
        /// If you use remoting appender the ExceptionObject is empty even if there
        /// was an exception but the <i>loggingEvent.GetExceptionString()</i> holds
        /// the exception string.<br/>
        /// So, to extract the exception type in that case, the full exception string
        /// is parsed. <i>sad i know :(</i>
        /// </remarks>
        /// <param name="loggingEvent">the event being logged</param>
        /// <returns>exception type full name string (string.empty if not found)</returns>
        private static string ExtractExceptionType(LoggingEvent loggingEvent)
        {
            string exceptionType = string.Empty;
            if (loggingEvent.ExceptionObject == null)
            {
                // get the exception type from the full exception string.
                string exceptionStr = loggingEvent.GetExceptionString();
                if (!string.IsNullOrEmpty(exceptionStr))
                {
                    // the type is the string from the beggining to a colon char.
                    int colonPos = exceptionStr.IndexOf(':');
                    if (colonPos > 0)
                        exceptionType = exceptionStr.Substring(0, colonPos);
                }
            }
            else
            {
                // we have exception object!!!
                exceptionType = loggingEvent.ExceptionObject.GetType().FullName;
            }
            return exceptionType;
        }

        /// <summary>
        /// Extract exception message.
        /// </summary>
        /// <remarks>
        /// If you use remoting appender the ExceptionObject is empty even if there
        /// was an exception but the <i>loggingEvent.GetExceptionString()</i> holds
        /// the exception string.<br/>
        /// So, to extract the exception message in that case, the full exception string
        /// is parsed. <i>sad i know :(</i>
        /// </remarks>
        /// <param name="loggingEvent">the event being logged</param>
        /// <returns>exeption message string (string.empty if not found)</returns>
        private static string ExtractExceptionMessage( LoggingEvent loggingEvent )
        {
            string exceptionMessage = string.Empty;
            if (loggingEvent.ExceptionObject == null)
            {
                // get the exception message from the full exception string.
                string exceptionStr = loggingEvent.GetExceptionString();
                if (!string.IsNullOrEmpty(exceptionStr))
                {
                    // the message is the string from the colon to newline.
                    int colonPos = exceptionStr.IndexOf(':');
                    int endPos = exceptionStr.IndexOf( Environment.NewLine );
                    if (colonPos > 0 && endPos > 0)
                        exceptionMessage = exceptionStr.Substring(colonPos+2, endPos-colonPos-2);
                }
            }
            else
            {
                // we have exception object!!!
                exceptionMessage = loggingEvent.ExceptionObject.Message;
            }
            return exceptionMessage;
        }

        #endregion


        #region Data members

        private ExceptionOutput mOutputType;

        /// <summary>
        /// get\set the output data type.
        /// Values: <see cref="ExceptionOutput"/>
        /// </summary>
        public string OutputType
        {
            get { return mOutputType.ToString(); }
            set
            {
                try
                {
                    mOutputType = (ExceptionOutput)Enum.Parse( typeof(ExceptionOutput), value, true );
                }
                catch( Exception )
                {
                    LogManager.WriteTrace( "Failed setting OutputType [value: " + value + "]" );
                    mOutputType = ExceptionOutput.None;
                }
            }
        }

        #endregion
    }

    #region ExceptionOutput enum

    /// <summary>
    /// enum of the possible output data types.
    /// </summary>
    public enum ExceptionOutput
    {
        None, // in case of error reading config
        Type,
        Message,
        Full
    }

    #endregion
}
